/*MyPhotoIndex Free Photo Organizer <www.myphotoindex.com>
  Copyright (C) <2006-2009>  <Asaf Yarkoni asaf@yarkoni.net>

  This program is free software: you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation, either version 3 of the License, or
  (at your option) any later version.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with this program.  If not, see <http://www.gnu.org/licenses/>.*/
using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;

namespace MyPhotoIndex.ImageCache
{
    public class ImageCacheInstance : IDisposable
    {
        public delegate void ProgressDelegate(int maximum, int value, object tag);

        const int SIZE_OF_RECORD = (sizeof(int) * 3) + sizeof(long);
        public const int DEFAULT_MAX_IMAGES = 1000001;
        const int NUM_IMAGES_COUNTER_POSITION = sizeof(int);
        const int FIRST_RECORD_POSITION = NUM_IMAGES_COUNTER_POSITION + sizeof(int);


        private Object m_syncRoot = new Object();
        private FileStream m_fileStream;
        BinaryReader m_reader;
        BinaryWriter m_writer;

        private int m_firstImageOffset;
        private string m_path;
        FileSystemWatcher m_fileWatcher;
        bool m_selfUpdate;

        private Dictionary<int, int> m_index = new Dictionary<int, int>();

        public ImageCacheInstance(string path, int maxNumberOfCachedImages, bool readOnly)
        {
            m_path = path;
            m_firstImageOffset = FIRST_RECORD_POSITION + (SIZE_OF_RECORD * maxNumberOfCachedImages);

            bool newCache = false;
            if (File.Exists(path) == false)
            {
                CreateNewCacheFile(path);
                newCache = true;
            }
            else
            {
                OpenCacheStreams(readOnly);
                newCache = false;
            }

            bool cacheOk = LoadIndex();
            if (cacheOk == false &&
                newCache == false)
            {
                CloseCacheStreams();

                try
                {
                    if (File.Exists(path) == true)
                    {
                        string dateString = string.Format("{0}{1}{2}{3}{4}{5}", new object[] { DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day, DateTime.Now.Hour, DateTime.Now.Minute, DateTime.Now.Second });
                        File.Move(path, path + dateString + ".old"); //keep old cache file;
                    }

                    CreateNewCacheFile(path);
                }
                catch { }

                OpenCacheStreams(readOnly);
            }

            m_fileWatcher = new FileSystemWatcher(System.IO.Path.GetDirectoryName(m_path), System.IO.Path.GetFileName(m_path));
            m_fileWatcher.Changed += new FileSystemEventHandler(m_fileWatcher_Changed);
        }

        private void CloseCacheStreams()
        {
            m_fileStream.Dispose();
            m_reader.Close();
            if (m_writer != null)
            {
                m_writer.Close();
            }
        }

        private void OpenCacheStreams(bool readOnly)
        {
            if (readOnly == false)
            {
                m_fileStream = new FileStream(m_path, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite);
            }
            else
            {
                m_fileStream = new FileStream(m_path, FileMode.Open, FileAccess.Read, FileShare.Read);
            }

            m_reader = new BinaryReader(m_fileStream);
            if (readOnly == false)
            {
                m_writer = new BinaryWriter(m_fileStream);
            }

            if (m_fileStream.Length == 0)
            {
                m_writer.Write((int)DEFAULT_MAX_IMAGES);
                m_writer.Write((int)0);
            }
        }

        public static void UpgradeCacheFile(string path, ProgressDelegate progressDelegate, object tag)
        {
            String tempFile = System.IO.Path.GetFullPath(path);
            tempFile = System.IO.Path.ChangeExtension(tempFile, ".tmp");

            using (Stream fileStream = new FileStream(path, FileMode.Open, FileAccess.ReadWrite, FileShare.None))
            {
                fileStream.Position = 0;
                BinaryReader reader = new BinaryReader(fileStream);
                BinaryWriter writer = new BinaryWriter(fileStream);

                using (FileStream tempFileStream = new FileStream(tempFile, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None))
                {
                    tempFileStream.Position = 0;
                    BinaryWriter tempWriter = new BinaryWriter(tempFileStream);
                    tempWriter.Write((int)DEFAULT_MAX_IMAGES); // add the max count to the new temp file

                    int numberOfRecords = reader.ReadInt32();
                    if (numberOfRecords == 1000000)
                    {//we are upgrading 1.24 beta to 1.24 beta with long position fix
                        // the record count in next 4 bytes
                        numberOfRecords = reader.ReadInt32();
                    }

                    Dictionary<int, ImageCacheRecord> recordsMap = new Dictionary<int, ImageCacheRecord>();
                    List<int> validRecords = new List<int>();

                    for (int ii = 0; ii < numberOfRecords; ii++)
                    {
                        int recordId = reader.ReadInt32();
                        int position = reader.ReadInt32();
                        int size = reader.ReadInt32();
                        int originalSize = reader.ReadInt32();

                        if (size != -1)
                        {
                            try
                            {
                                validRecords.Add(recordId);
                                recordsMap.Add(recordId, new ImageCacheRecord(recordId, position, size, originalSize));
                            }
                            catch { }
                        }
                    }

                    validRecords.Sort(new Comparison<int>(compareIds));
                    tempWriter.Write(validRecords.Count);

                    int recordIndex = 0;
                    int firstImageOffset = FIRST_RECORD_POSITION + (SIZE_OF_RECORD * DEFAULT_MAX_IMAGES);
                    long bufferOffset = firstImageOffset;
                    foreach (int recordId in validRecords)
                    {
                        tempFileStream.Position = (recordIndex * SIZE_OF_RECORD) + FIRST_RECORD_POSITION;
                        ImageCacheRecord imageRecord = recordsMap[recordId];
                        tempWriter.Write(imageRecord.Id);
                        tempWriter.Write(bufferOffset);
                        tempWriter.Write(imageRecord.Size);
                        tempWriter.Write(imageRecord.Size);

                        byte[] data = new byte[imageRecord.Size];
                        fileStream.Position = imageRecord.Position;
                        fileStream.Read(data, 0, imageRecord.Size);

                        tempFileStream.Position = bufferOffset;
                        tempWriter.Write(data);

                        bufferOffset += imageRecord.Size;
                        recordIndex++;

                        if (progressDelegate != null)
                        {
                            progressDelegate.Invoke(validRecords.Count, recordIndex, tag);
                        }
                    }
                }
            }

            string dateString = string.Format("{0}{1}{2}{3}{4}{5}", new object[] { DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day, DateTime.Now.Hour, DateTime.Now.Minute, DateTime.Now.Second });
            File.Move(path, path + dateString + ".old"); //keep old cache file
            File.Move(tempFile, path);
        }

        public static int GetCahceMaxSize(string path)
        {
            using (Stream fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
            using (BinaryReader reader = new BinaryReader(fileStream))
            {
                if (fileStream.Length <= 0)
                {
                    return 0;
                }

                int size = reader.ReadInt32();
                if (size == 1000000)
                {//we are upgrading 1.24 beta to 1.24 beta with long position fix
                    // this will be removed with the release version
                    size = 999999;
                }

                return size;
            }
        }

        void m_fileWatcher_Changed(object sender, FileSystemEventArgs e)
        {
            if (m_selfUpdate == false)
            {
                LoadIndex();
            }
        }

        public ImageCacheInstance(string path)
            :
            this(path, DEFAULT_MAX_IMAGES, false)
        {
        }

        private void CreateNewCacheFile(string path)
        {
            m_fileStream = new FileStream(path, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.ReadWrite);
            m_reader = new BinaryReader(m_fileStream);
            m_writer = new BinaryWriter(m_fileStream);

            m_writer.Write((int)DEFAULT_MAX_IMAGES);
            m_writer.Write((int)0);
        }

        ~ImageCacheInstance()
        {
            m_fileStream.Dispose();
        }

        private bool LoadIndex()
        {
            m_index.Clear();

            lock (m_syncRoot)
            {
                m_fileStream.Position = NUM_IMAGES_COUNTER_POSITION;
                int numberOfRecords = m_reader.ReadInt32();
                if (numberOfRecords > DEFAULT_MAX_IMAGES ||
                    numberOfRecords < 0)
                {// this is not good seams like the cache got corrupted....
                    return false;
                }

                for (int ii = 0; ii < numberOfRecords; ii++)
                {
                    int streamPosition = (int)m_fileStream.Position;

                    int recordId = m_reader.ReadInt32();
                    long position = m_reader.ReadInt64();
                    int size = m_reader.ReadInt32();
                    int originalSize = m_reader.ReadInt32();

                    if (size != -1)
                    {
                        try
                        {
                            if (m_index.ContainsKey(recordId) == false)
                            {
                                m_index.Add(recordId, streamPosition);
                            }
                        }
                        catch { }
                    }
                }
            }

            return true;
        }

        public byte[] Find(int id)
        {
            using (ImageCacheStream cacheStream = FindStream(id))
            {
                if (cacheStream == null)
                {
                    return null;
                }

                byte[] bytes = new byte[cacheStream.Length];
                cacheStream.Read(bytes, 0, (int)cacheStream.Length);

                return bytes;
            }
        }

        public ImageCacheStream FindStream(int id)
        {
            try
            {
                int recordPosition = 0;
                lock (m_syncRoot)
                {
                    if (m_index.TryGetValue(id, out recordPosition) == false)
                    {
                        return null;
                    }
                }

                Stream fs = new FileStream(m_path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
                BinaryReader sr = new BinaryReader(fs);
                {
                    fs.Position = recordPosition;

                    int recordId = sr.ReadInt32();
                    long position = sr.ReadInt64();
                    int size = sr.ReadInt32();
                    int originalSize = sr.ReadInt32();

                    if (size == -1)
                    {
                        fs.Dispose();
                        return null;
                    }

                    return new ImageCacheStream(fs, position, size);
                }
            }
            catch { }

            return null;
        }

        public bool IsExist(int id)
        {
            lock (m_syncRoot)
            {
                return m_index.ContainsKey(id);
            }
        }

        public ImageCacheRecord GetCacheRecord(int id)
        {
            lock (m_syncRoot)
            {
                int recordPosition = 0;
                if (m_index.TryGetValue(id, out recordPosition) == false)
                {
                    return null;
                }

                m_fileStream.Position = recordPosition;
                int recordId = m_reader.ReadInt32();

                long position = m_reader.ReadInt64();
                int size = m_reader.ReadInt32();
                int originalSize = m_reader.ReadInt32();

                return new ImageCacheRecord(recordId, position, size, originalSize);
            }
        }

        public void Add(int id, byte[] data)
        {
            Add(id, data, true);
        }

        internal void Add(int id, byte[] data, bool checkIfExist)
        {
            if (checkIfExist == true)
            {
                if (IsExist(id) == true)
                {
                    Delete(id);
                }
            }

            lock (m_syncRoot)
            {
                m_selfUpdate = true;
                //get the # of records
                m_fileStream.Position = NUM_IMAGES_COUNTER_POSITION;
                int numberOfRecords = m_reader.ReadInt32();

                //Add Record
                ImageCacheRecord emptyPos = GetFreeWritePosition(id, data.Length, numberOfRecords);
                int recordPosition = (int)m_fileStream.Position - SIZE_OF_RECORD;
                m_fileStream.Position = emptyPos.Position;
                m_fileStream.Write(data, 0, data.Length);

                //update the # of records 
                if (emptyPos.NewRecord == true)
                {
                    m_fileStream.Position = NUM_IMAGES_COUNTER_POSITION;
                    m_writer.Write(++numberOfRecords);
                }

                m_index.Add(id, recordPosition);
                m_selfUpdate = false;
            }
        }

        public void Delete(int id)
        {
            lock (m_syncRoot)
            {
                int recordPosition = 0;
                if (m_index.TryGetValue(id, out recordPosition) == false)
                {
                    return;
                }

                m_selfUpdate = true;
                m_fileStream.Position = recordPosition;

                int recordId = m_reader.ReadInt32();
                long position = m_reader.ReadInt64();
                int size = m_reader.ReadInt32();
                int originalSize = m_reader.ReadInt32();

                if (size != -1)
                {
                    m_fileStream.Position = recordPosition;

                    m_writer.Write((int)-1);
                    m_writer.Write(position);
                    m_writer.Write((int)-1);
                    m_writer.Write(originalSize);
                }

                m_index.Remove(id);
                m_selfUpdate = false;
            }
        }

        private ImageCacheRecord GetFreeWritePosition(int newId, int bufferSize, int numOfRecords)
        {
            ImageCacheRecord ifci = null;

            m_fileStream.Position = FIRST_RECORD_POSITION;// move to the first record
            for (int ii = 0; ii < numOfRecords; ii++)
            {
                long recordPosition = m_fileStream.Position;
                ifci = GetRecordByOffset();

                if (-1 == ifci.Size &&
                    ifci.OriginalSize >= bufferSize)
                {
                    m_fileStream.Position = recordPosition;
                    break;
                }
                else
                {
                    ifci = null;
                }
            }

            if (ifci == null)
            {
                ifci = new ImageCacheRecord(newId, -1, bufferSize, bufferSize);
                if (numOfRecords == 0)
                {
                    ifci.Position = m_firstImageOffset;
                }
                else
                {
                    ImageCacheRecord tempLfci = GetRecordByOffset(FIRST_RECORD_POSITION + ((numOfRecords - 1) * SIZE_OF_RECORD));
                    ifci.Position = tempLfci.Position + tempLfci.OriginalSize;
                }
            }
            else
            {
                ifci.Id = newId;
                ifci.Size = bufferSize;
                ifci.NewRecord = false;
            }

            m_writer.Write(ifci.Id);
            m_writer.Write(ifci.Position);
            m_writer.Write(ifci.Size);
            m_writer.Write(ifci.OriginalSize);

            return ifci;
        }

        private ImageCacheRecord GetRecordByOffset(int recordPosition)
        {
            m_fileStream.Position = recordPosition;
            return GetRecordByOffset();
        }

        private ImageCacheRecord GetRecordByOffset()
        {
            //   BinaryReader reader = new BinaryReader(m_fileStream);
            int id = m_reader.ReadInt32();
            long position = m_reader.ReadInt64();
            int size = m_reader.ReadInt32();
            int originalSize = m_reader.ReadInt32();

            return new ImageCacheRecord(id, position, size, originalSize);
        }

        public int Count
        {
            get
            {
                return m_index.Count;
            }
        }

        public string Path
        {
            get
            {
                return m_path;
            }
        }

        public void Reindex(ProgressDelegate progressDelegate, object tag)
        {
            lock (m_syncRoot)
            {
                String tempFile = System.IO.Path.GetFullPath(Path);
                tempFile = System.IO.Path.ChangeExtension(tempFile, ".tmp");

                m_selfUpdate = true;
                m_fileWatcher.Dispose();
                m_fileStream.Dispose();
                using (m_fileStream = new FileStream(m_path, FileMode.Open, FileAccess.ReadWrite, FileShare.None))
                {
                    m_fileStream.Position = NUM_IMAGES_COUNTER_POSITION;
                    m_reader = new BinaryReader(m_fileStream);
                    m_writer = new BinaryWriter(m_fileStream);

                    using (FileStream tempFileStream = new FileStream(tempFile, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None))
                    {
                        BinaryWriter tempWriter = new BinaryWriter(tempFileStream);

                        m_fileStream.Position = NUM_IMAGES_COUNTER_POSITION;
                        int numberOfRecords = m_reader.ReadInt32();

                        Dictionary<int, ImageCacheRecord> recordsMap = new Dictionary<int, ImageCacheRecord>();
                        List<int> validRecords = new List<int>();

                        for (int ii = 0; ii < numberOfRecords; ii++)
                        {
                            int recordId = m_reader.ReadInt32();
                            long position = m_reader.ReadInt64();
                            int size = m_reader.ReadInt32();
                            int originalSize = m_reader.ReadInt32();

                            if (size != -1)
                            {
                                try
                                {
                                    validRecords.Add(recordId);
                                    recordsMap.Add(recordId, new ImageCacheRecord(recordId, position, size, originalSize));
                                }
                                catch { }
                            }
                        }

                        validRecords.Sort(new Comparison<int>(compareIds));
                        //initialize the cache file
                        tempFileStream.Position = 0;
                        tempWriter.Write((int)DEFAULT_MAX_IMAGES);
                        tempWriter.Write(validRecords.Count);

                        int recordIndex = 0;
                        long bufferOffset = m_firstImageOffset;
                        foreach (int recordId in validRecords)
                        {
                            tempFileStream.Position = (recordIndex * SIZE_OF_RECORD) + FIRST_RECORD_POSITION;
                            ImageCacheRecord imageRecord = recordsMap[recordId];
                            tempWriter.Write(imageRecord.Id);
                            tempWriter.Write(bufferOffset);
                            tempWriter.Write(imageRecord.Size);
                            tempWriter.Write(imageRecord.Size);

                            byte[] data = new byte[imageRecord.Size];
                            m_fileStream.Position = imageRecord.Position;
                            m_fileStream.Read(data, 0, imageRecord.Size);

                            tempFileStream.Position = bufferOffset;
                            tempWriter.Write(data);

                            bufferOffset += imageRecord.Size;
                            recordIndex++;

                            if (progressDelegate != null)
                            {
                                progressDelegate.Invoke(validRecords.Count, recordIndex, tag);
                            }
                        }
                    }
                }

                string dateString = string.Format("{0}{1}{2}{3}{4}{5}", new object[] { DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day, DateTime.Now.Hour, DateTime.Now.Minute, DateTime.Now.Second });
                File.Move(m_path, m_path + dateString + ".old"); //keep old cache file
                File.Move(tempFile, m_path);

                m_fileStream = new FileStream(m_path, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite);
                m_reader = new BinaryReader(m_fileStream);
                m_writer = new BinaryWriter(m_fileStream);
                LoadIndex();

                m_selfUpdate = false;
                m_fileWatcher = new FileSystemWatcher(System.IO.Path.GetDirectoryName(m_path), System.IO.Path.GetFileName(m_path));
                m_fileWatcher.Changed += new FileSystemEventHandler(m_fileWatcher_Changed);
            }
        }

        private static int compareIds(int a, int b)
        {
            return b - a;
        }

        public long GetPhysicalSizeBytes()
        {
            long totalBytes = 0;
            lock (m_syncRoot)
            {
                m_fileStream.Position = NUM_IMAGES_COUNTER_POSITION;
                int numberOfRecords = m_reader.ReadInt32();

                for (int ii = 0; ii < numberOfRecords; ii++)
                {
                    int streamPosition = (int)m_fileStream.Position;

                    int recordId = m_reader.ReadInt32();
                    long position = m_reader.ReadInt64();
                    int size = m_reader.ReadInt32();
                    int originalSize = m_reader.ReadInt32();

                    totalBytes += originalSize;
                }
            }

            return totalBytes;
        }

        public long GetFreeSpaceBytes()
        {
            long totalBytes = 0;
            lock (m_syncRoot)
            {
                m_fileStream.Position = NUM_IMAGES_COUNTER_POSITION;
                int numberOfRecords = m_reader.ReadInt32();

                for (int ii = 0; ii < numberOfRecords; ii++)
                {
                    int streamPosition = (int)m_fileStream.Position;

                    int recordId = m_reader.ReadInt32();
                    long position = m_reader.ReadInt64();
                    int size = m_reader.ReadInt32();
                    int originalSize = m_reader.ReadInt32();

                    if (size != -1)
                    {
                        totalBytes += originalSize - size;
                    }
                    else
                    {
                        totalBytes += originalSize;
                    }
                }
            }

            return totalBytes;
        }

        #region IDisposable Members

        public void Dispose()
        {
            if (m_fileWatcher != null)
            {
                m_fileWatcher.Dispose();
            }

            if (m_fileStream != null)
            {
                m_fileStream.Dispose();
            }
        }

        #endregion
    }
}
